package dd.lo.chapter2;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.geometry.Orientation;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.Slider;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import org.opencv.core.*;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import org.opencv.videoio.VideoCapture;
import org.opencv.videoio.Videoio;

import java.io.ByteArrayInputStream;
import java.util.ArrayList;
import java.util.List;

/**
 * Example3是一个类似于视频播放器的程序，对应书中的Example 2-4. Adding a trackbar slider to the basic viewer window for moving around within the video file
 * 关于读取视频信息的一些常量，在JAVA中是Videoio类
 * https://stackoverflow.com/questions/21066875/opencv-constants-captureproperty
 */
public class Example3 extends Application {

    private VideoCapture cap;
    private int totalFrameCnt;

    private List<Mat> frameCahce;

    private int fpsConfig;
    //当前播放到的帧数
    private int curPosition;
    private volatile boolean playing;
    private int w;
    private int h;

    private Label frameText;
    private Label fpsText;
    private Button playBtn;
    private Slider slider;
    private ImageView imageView;

    private Mat frame;

    public static void main(String[] args) {
        String path = Example3.class.getResource("/libopencv_java481.dylib").getPath();
        System.out.println(path);
        System.load(path);
        launch(args);
    }

    @Override
    public void start(Stage stage) {
        initialize();
        //组织整个页面的组件，bottomPane包含播放按钮、Slider、帧数显示文本三个元素
        int sliderBarHeight = 30;
        frameText = new Label(String.format("%d/%d", curPosition, totalFrameCnt));
        fpsText = new Label(String.format("FPS: %d", fpsConfig));
        frameText.setPrefWidth((double) w * 0.06);
        playBtn = new Button(playing ? "Pause" : "Play");
        playBtn.setPrefWidth((double) w * 0.06);
        playBtn.setOnAction(actionEvent -> {
            if (!playing) {
                play();
            } else {
                pause();
            }
        });
        playBtn.setDisable(true);
        slider = new Slider(0, totalFrameCnt, 0);
        slider.setPrefWidth((double) w * 0.77);
        slider.valueProperty().addListener((observableValue, ov, nv) -> {
            frameText.setText(String.format("%d/%d", nv.intValue(), totalFrameCnt));
            if (!playing) curPosition = nv.intValue();
        });
        slider.setOnMouseReleased(mouseEvent -> {
            updateFrameImage();
//            play();
        });
        slider.setOnMouseDragged(mouseEvent -> {
            pause();
            curPosition = (int) slider.getValue();
        });
        FlowPane bottomPane = new FlowPane();
        bottomPane.setOrientation(Orientation.HORIZONTAL);
        bottomPane.setPrefWidth(w);
        bottomPane.setPrefHeight(sliderBarHeight);
        bottomPane.setBorder(new Border(new BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, new BorderWidths(1))));
        bottomPane.getChildren().add(playBtn);
        bottomPane.getChildren().add(slider);
        bottomPane.getChildren().add(frameText);
        bottomPane.getChildren().add(fpsText);
        //centerPane主要是视频帧的显示元素
        imageView = new ImageView();
        imageView.setFitWidth(w);
        imageView.setFitHeight(h);
        FlowPane centerPane = new FlowPane();
        centerPane.setPrefWidth(w);
        centerPane.setPrefHeight(h - sliderBarHeight);
        centerPane.getChildren().add(imageView);
        centerPane.setBorder(new Border(new BorderStroke(Color.RED, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, new BorderWidths(1))));
        BorderPane root = new BorderPane();
        root.setBottom(bottomPane);
        root.setCenter(centerPane);
        Scene scene = new Scene(root, w, h + sliderBarHeight);
        stage.setScene(scene);
        stage.setTitle("Example3");
        stage.setResizable(false);
        stage.show();
    }

    @Override
    public void stop() throws Exception {
        if (null != cap) {
            cap.release();
        }
        super.stop();
    }

    /**
     * 初始化，读取VideoCapture中的视频信息
     */
    private void initialize() {
        this.frame = new Mat();
        this.cap = new VideoCapture("/Users/kwok/Downloads/young_women_listening_to_music.mp4");
        this.totalFrameCnt = (int) cap.get(Videoio.CAP_PROP_FRAME_COUNT);
        this.fpsConfig = (int) cap.get(Videoio.CAP_PROP_FPS);
        this.curPosition = 0;
        this.playing = false;
        this.w = (int) cap.get(Videoio.CAP_PROP_FRAME_WIDTH) / 2;
        this.h = (int) cap.get(Videoio.CAP_PROP_FRAME_HEIGHT) / 2;
        this.frameCahce = new ArrayList<>(totalFrameCnt);
        System.out.printf("Video has %d frames (fps: %d) of dimensions(%d, %d)\n", totalFrameCnt, fpsConfig, w, h);
        Task<Void> task = new Task<Void>() {
            @Override
            protected Void call() {
                preLoadVideo();
                return null;
            }
        };
        new Thread(task).start();
    }

    private void preLoadVideo() {
        long start = System.currentTimeMillis();
        System.out.println("Start loading frame and store in queue...");
        do {
            //用OpenCV读取一帧图像，并自动给cap的当前帧数+1，并缓存这一帧图像
            cap.read(frame);
            Mat f = new Mat();
            frame.copyTo(f);
            frameCahce.add(f);
        } while (!frame.empty());
        Platform.runLater(() -> playBtn.setDisable(false));
        long time = System.currentTimeMillis() - start;
        System.out.printf("Finish preLoading in %dms, that's equal to %ffps\n", time, (double) totalFrameCnt * 1000 / time);
        for (int i = 0; i < totalFrameCnt; ++i) {
            Mat f = frameCahce.get(i);
            Imgproc.resize(f, f, new Size(w, h));
            frameCahce.set(i, f);
        }
        System.out.println("Finish preresizing frames!");
    }

    private void play() {
        if (playing) return;
        //开始播放，修改一些状态，playing，curPosition当前播放帧数
        playing = true;
        if (curPosition >= totalFrameCnt - 1) curPosition = 0;
        //buffer用于保存OpenCV读取出来的视频帧的编码后的图像信息
        MatOfByte buffer = new MatOfByte();
        //将视频定位到curPosition当前播放帧数
        cap.set(Videoio.CAP_PROP_POS_FRAMES, curPosition);
        //下面将更新视频图像的工作交由一个异步线程执行，如果在UI线程（主线程）中执行图像更新操作，则会一直卡死程序
        Task<Void> task = new Task<Void>() {
            @Override
            protected Void call() {
                long startTime = System.currentTimeMillis();
                int frameCnt = 0;
                int fps;
                //注意，在异步线程中如果直接修改主线程中的元素，则会报错说不在主线程中修改，因此需要用Platform.runLater来通知主线程更新元素
                Platform.runLater(() -> playBtn.setText("Pause"));
                while (true) {
                    if (!playing) break;
                    if (curPosition >= totalFrameCnt) {
                        //到达视频结尾，暂停并结束视频播放任务
                        Platform.runLater(() -> pause());
                        break;
                    }
                    //从缓存中读取一帧图像
                    Mat f = frameCahce.get(curPosition++);
                    ++frameCnt;
                    if (f.empty()) {
                        //如果读不出图像，就可以认为是到了视频结尾，暂停并结束视频播放任务
                        Platform.runLater(() -> pause());
                        break;
                    }
                    long cur = System.currentTimeMillis();
                    if (cur - startTime > 1000L) {
                        startTime = cur;
                        fps = frameCnt;
                        frameCnt = 0;
                        final int fpsNumber = fps;
                        Platform.runLater(() -> fpsText.setText(String.format("FPS: %d", fpsNumber)));
                    }
//                    Imgproc.resize(f, f, new Size(w, h));
                    //将FPS绘制到图像帧上
//                    Imgproc.putText(f, String.format("FPS: %d", fps), new Point(10, 40), Imgproc.FONT_HERSHEY_SIMPLEX, 1.0, new Scalar(249, 105, 48), 1, Imgproc.LINE_8);
                    //读取到帧数据，将帧的Mat转换为MatOfBuffer
                    Imgcodecs.imencode(".png", f, buffer);
                    Platform.runLater(() -> {
                        //需要修改ImageView，因此也放到Platform.runLater中更新，此时可以用ByteArrayInputStream转换MatOfByte来显示到ImageView中
                        imageView.setImage(new Image(new ByteArrayInputStream(buffer.toArray())));
                        slider.setValue(curPosition);
                    });
                }
                System.out.println("Play task is interrupted or finished.");
                return null;
            }
        };
        new Thread(task).start();
    }

    private void updateFrameImage() {
        Task<Void> task = new Task<Void>() {
            @Override
            protected Void call() {
                if (curPosition >= totalFrameCnt) return null;
                frame = frameCahce.get(curPosition);
                if (!frame.empty()) {
                    MatOfByte buffer = new MatOfByte();
                    Imgcodecs.imencode(".png", frame, buffer);
                    Platform.runLater(() -> imageView.setImage(new Image(new ByteArrayInputStream(buffer.toArray()))));
                }
                return null;
            }
        };
        new Thread(task).start();
    }

    private void pause() {
        playing = false;
        playBtn.setText("Play");
    }
}
